---
title: React Suspense를 위한 모든 것
---

import Image from 'next/image'
import Link from 'next/link'
import {
  HomePage,
  Scrollycoding,
  SectionDescription,
  SectionTitle,
  TrustedBy,
} from '@/components'

<HomePage
  buttonText="시작하기"
  items={[
    {
      title: '모든 선언적 API를 제공',
      desc: '`<Suspense/>`, `<ErrorBoundary/>`, `<ErrorBoundaryGroup/>` 등을 제공합니다. 별 다른 노력없이 쉽게 사용할 수 있습니다.',
    },
    {
      title: 'Zero 의존성, 오직 React',
      desc: '단순히 React의 개념을 확장한 것입니다. `<Suspense/>`, `<ErrorBoundary/>`, `<ErrorBoundaryGroup/>`와 같은 React 개발자에게 친숙한 이름으로 컴포넌트들을 제공합니다.',
    },
    {
      title: '서버 사이드 렌더링에서도 쉽게',
      desc: 'Suspensive는 개발자가 서버 사이드 렌더링 환경에서도 React Suspense를 점진적으로 채택할 수 있도록 clientOnly를 제공합니다.',
    },
  ]}
>

<SectionTitle>Suspense를 사용하기 위한 Best Practice를 제공합니다</SectionTitle>
<br />
<SectionDescription>
  에러와 로딩은 Suspensive에 맡기고 성공한 케이스에 집중하세요
</SectionDescription>
<br />

<Scrollycoding>

# !!steps 대표적인 라이브러리인 TanStack Query로 Suspense 없이 코드를 작성한다면 이렇게 작성합니다.

이 경우 `isLoading`과 `isError`를 체크하여 로딩과 에러 상태를 처리하고 타입스크립트적으로 data에서 `undefined`를 제거할 수 있습니다.

```jsx ! Page.jsx
import { useQuery } from '@tanstack/react-query'

const Page = () => {
  const userQuery = useQuery(userQueryOptions())
  const postsQuery = useQuery({
    ...postsQueryOptions(),
    select: (posts) => posts.filter(({ isPublic }) => isPublic),
  })
  const promotionsQuery = useQuery(promotionsQueryOptions())

  if (
    userQuery.isLoading ||
    postsQuery.isLoading ||
    promotionsQuery.isLoading
  ) {
    return 'loading...'
  }

  if (userQuery.isError || postsQuery.isError || promotionsQuery.isError) {
    return 'error'
  }

  return (
    <Fragment>
      <UserProfile {...userQuery.data} />
      {postsQuery.data.map((post) => (
        <PostListItem key={post.id} {...post} />
      ))}
      {promotionsQuery.data.map((promotion) => (
        <Promotion key={promotion.id} {...promotion} />
      ))}
    </Fragment>
  )
}
```

# !!steps 그런데 만약 조회해야 할 api가 더 많아진다고 가정해봅시다.

조회해야 하는 API가 더 많아진다면 이 로딩상태와 에러상태를 처리하는 코드가 더욱 복잡해집니다.

```jsx ! Page.jsx
import { useQuery } from '@tanstack/react-query'

const Page = () => {
  const userQuery = useQuery(userQueryOptions())
  const postsQuery = useQuery({
    ...postsQueryOptions(),
    select: (posts) => posts.filter(({ isPublic }) => isPublic),
  })
  const promotionsQuery = useQuery(promotionsQueryOptions())
  // 이 부분이 늘어날수록 코드가 복잡해집니다.

  if (
    userQuery.isLoading ||
    postsQuery.isLoading ||
    promotionsQuery.isLoading // 이 부분에서 매번 추가해야 합니다.
  ) {
    return 'loading...'
  }

  if (
    userQuery.isError ||
    postsQuery.isError ||
    promotionsQuery.isError // 이 부분에서 매번 추가해야 합니다.
  ) {
    return 'error'
  }

  return (
    <Fragment>
      <UserProfile {...userQuery.data} />
      {postsQuery.data.map((post) => (
        <PostListItem key={post.id} {...post} />
      ))}
      {promotionsQuery.data.map((promotion) => (
        <Promotion key={promotion.id} {...promotion} />
      ))}
      {/* 이 부분에서 매번 추가해야 합니다. */}
    </Fragment>
  )
}
```

# !!steps Suspense를 사용하면 타입적으로 코드가 간결해집니다. 하지만 컴포넌트의 깊이는 깊어질 수 밖에 없습니다.

`useSuspenseQuery`는 `Suspense`와 `ErrorBoundary`를 사용하여 외부에서 로딩과 에러 상태를 처리할 수 있습니다.
하지만 `useSuspenseQuery`는 hook이기 때문에 부모에 `Suspense`와 `ErrorBoundary`를 두기 위해 컴포넌트가 분리되어야만 하기 때문에 뎁스가 깊어지는 문제가 있습니다.

```jsx ! Page.jsx
import { ErrorBoundary, Suspense } from '@suspensive/react'
import { useSuspenseQuery } from '@tanstack/react-query'

const Page = () => (
  <ErrorBoundary fallback="error">
    <Suspense fallback="loading...">
      <UserInfo userId={userId} />
      <PostList userId={userId} />
      <PromotionList userId={userId} />
    </Suspense>
  </ErrorBoundary>
)

const UserInfo = ({ userId }) => {
  const { data: user } = useSuspenseQuery(userQueryOptions())
  return <UserProfile {...user} />
}

const PostList = ({ userId }) => {
  const { data: posts } = useSuspenseQuery({
    ...postsQueryOptions(),
    select: (posts) => posts.filter(({ isPublic }) => isPublic),
  })
  return posts.map((post) => <PostListItem key={post.id} {...post} />)
}

const PromotionList = ({ userId }) => {
  const { data: promotions } = useSuspenseQuery(promotionsQueryOptions())
  return promotions.map((promotion) => (
    <PromotionListItem key={promotion.id} {...promotion} />
  ))
}
```

# !!steps Suspensive의 SuspenseQuery 컴포넌트를 사용하면 hook의 제약을 피해 같은 뎁스에서 더욱 쉽게 코드를 작성할 수 있습니다.

1. `SuspenseQuery`를 사용하면 depth를 제거할 수 있습니다.
2. UserInfo라는 컴포넌트를 제거하고 UserProfile과 같은 Presentational 컴포넌트만 남으므로 테스트하기 쉬워집니다.

```jsx ! Page.jsx
import { ErrorBoundary, Suspense } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const Page = () => (
  <ErrorBoundary fallback="error">
    <Suspense fallback="loading...">
      <SuspenseQuery {...userQueryOptions()}>
        {({ data: user }) => <UserProfile {...user} />}
      </SuspenseQuery>
      <SuspenseQuery
        {...postsQueryOptions()}
        select={(posts) => posts.filter(({ isPublic }) => isPublic)}
      >
        {({ data: posts }) =>
          posts.map((post) => <PostListItem key={post.id} {...post} />)
        }
      </SuspenseQuery>
      <SuspenseQuery
        {...promotionsQueryOptions()}
        select={(promotions) => promotions.filter(({ isPublic }) => isPublic)}
      >
        {({ data: promotions }) =>
          promotions.map((promotion) => (
            <PromotionListItem key={promotion.id} {...promotion} />
          ))
        }
      </SuspenseQuery>
    </Suspense>
  </ErrorBoundary>
)
```

</Scrollycoding>

<SectionTitle>이것이 우리가 Suspensive를 만드는 이유입니다</SectionTitle>
<br />
<SectionDescription>Suspense, ClientOnly, DefaultProps</SectionDescription>
<br />

<Scrollycoding>

# !!steps Next.js와 같은 프레임워크를 사용하다보면 서버에서 Suspense를 사용하기 어려울 때가 있습니다.

혹은 서버에서 `Suspense`를 사용하고 싶지 않을 때가 있습니다.

```jsx ! Page.jsx
import { Suspense } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const Page = () => (
  <Suspense fallback={<Spinner />}>
    <SuspenseQuery {...notNeedSEOQueryOptions()}>
      {({ data }) => <NotNeedSEO {...data} />}
    </SuspenseQuery>
  </Suspense>
)
```

# !!steps 이럴 때 Suspensive의 ClientOnly를 사용하면 쉽게 해결할 수 있습니다.

`ClientOnly`를 감싸주기만 하면 해결됩니다.

```jsx ! Page.jsx
import { Suspense, ClientOnly } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const Page = () => (
  <Suspense fallback={<Spinner />}>
    <ClientOnly fallback={<InsteadOfChildrenOnlyInServer />}>
      <SuspenseQuery {...notNeedSEOQueryOptions()}>
        {({ data }) => <NotNeedSEO {...data} />}
      </SuspenseQuery>
    </ClientOnly>
  </Suspense>
)
```

# !!steps 혹은 Suspensive의 Suspense에는 clientOnly prop을 활용해 이러한 경우를 쉽게 대응할 수 있습니다.

간단하죠?

```jsx ! Page.jsx
import { Suspense } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const Page = () => (
  <Suspense fallback={<Spinner />} clientOnly>
    <SuspenseQuery {...notNeedSEOQueryOptions()}>
      {({ data }) => <NotNeedSEO {...data} />}
    </SuspenseQuery>
  </Suspense>
)
```

# !!steps 그런데 개발을 하다보면 Suspense에 일일이 fallback을 넣어주기 어려울 때가 있습니다.

특히 Admin과 같은 제품을 개발하다 보면 디자이너가 일일이 지정해 주지 않는 경우가 있어서 기본값을 주고 싶을 때가 있습니다.
그럴 때 `DefaultProps`를 활용해보세요.

```jsx ! Page.jsx
import { Suspense, DefaultProps, DefaultPropsProvider } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const defaultProps = new DefaultProps({
  Suspense: {
    fallback: <Spinner />,
  },
})

const Page = () => (
  <DefaultPropsProvider defaultProps={defaultProps}>
    <Suspense clientOnly>
      <SuspenseQuery {...notNeedSEOQueryOptions()}>
        {({ data }) => <NotNeedSEO {...data} />}
      </SuspenseQuery>
    </Suspense>
  </DefaultPropsProvider>
)
```

# !!steps 당연히 기본 fallback을 Override하고 싶다면 그냥 넣어주면 됩니다.

디자이너가 이 부분에 기본 `Spinner`보다 `Skeleton`으로 동작하도록 지원해달라고 하네요~! 그냥 넣으면 되어요.

```jsx ! Page.jsx
import { Suspense, DefaultProps, DefaultPropsProvider } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const defaultProps = new DefaultProps({
  Suspense: {
    fallback: <Spinner />,
  },
})

const Page = () => (
  <DefaultPropsProvider defaultProps={defaultProps}>
    <Suspense fallback={<Skeleton />} clientOnly>
      <SuspenseQuery {...notNeedSEOQueryOptions()}>
        {({ data }) => <NotNeedSEO {...data} />}
      </SuspenseQuery>
    </Suspense>
  </DefaultPropsProvider>
)
```

</Scrollycoding>

<SectionDescription>ErrorBoundary, ErrorBoundaryGroup</SectionDescription>
<br />

<Scrollycoding>

# !!steps ErrorBoundary의 fallback 외부에서 ErrorBoundary를 reset하고 싶을 때 resetKeys를 사용해야 합니다.

이는 깊은 컴포넌트의 경우 `resetKey`를 전달해야 하는 문제가 있습니다. 또한 state를 만들어 `resetKey`를 전달해야 하는 문제가 있습니다.

```jsx ! Page.jsx
import { ErrorBoundary } from '@suspensive/react'

const Page = () => {
  const [resetKey, setResetKey] = useState(0)

  return (
    <Fragment>
      <button onClick={() => setResetKey((prev) => prev + 1)}>
        error reset
      </button>
      <ErrorBoundary resetKeys={[resetKey]} fallback="error">
        <ThrowErrorComponent />
      </ErrorBoundary>
      <DeepComponent resetKeys={[resetKey]} />
    </Fragment>
  )
}

const DeepComponent = ({ resetKeys }) => (
  <ErrorBoundary resetKeys={resetKeys} fallback="error">
    <ThrowErrorComponent />
    <ErrorBoundary resetKeys={resetKeys} fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
  </ErrorBoundary>
)
```

# !!steps Suspensive가 제공하는 ErrorBoundary와 ErrorBoundaryGroup을 조합하면 이러한 문제를 매우 단순히 해결할 수 있습니다.

`ErrorBoundaryGroup`을 사용해보세요.

```jsx ! Page.jsx
import { ErrorBoundary, ErrorBoundaryGroup } from '@suspensive/react'

const Page = () => (
  <ErrorBoundaryGroup>
    <ErrorBoundaryGroup.Consumer>
      {({ reset }) => <button onClick={reset}>error reset</button>}
    </ErrorBoundaryGroup.Consumer>
    <ErrorBoundary fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
    <DeepComponent />
  </ErrorBoundaryGroup>
)

const DeepComponent = () => (
  <ErrorBoundary fallback="error">
    <ThrowErrorComponent />
    <ErrorBoundary fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
  </ErrorBoundary>
)
```

# !!steps 그런데 ErrorBoundary를 사용하다보면 특정 Error에 대해서만 처리하고 싶을 때가 있습니다.

그럴 때에는 Suspensive가 제공하는 `ErrorBoundary`의 `shouldCatch`를 써보세요. 이 `shouldCatch`에 Error Constructor를 넣으면 해당 Error에 대해서만 처리할 수 있습니다.

```jsx ! Page.jsx
import { ErrorBoundary, ErrorBoundaryGroup } from '@suspensive/react'

const Page = () => (
  <ErrorBoundaryGroup>
    <ErrorBoundaryGroup.Consumer>
      {({ reset }) => <button onClick={reset}>error reset</button>}
    </ErrorBoundaryGroup.Consumer>
    <ErrorBoundary fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
    <DeepComponent />
  </ErrorBoundaryGroup>
)

const DeepComponent = () => (
  <ErrorBoundary fallback="error">
    <ThrowErrorComponent />
    <ErrorBoundary
      shouldCatch={ZodError}
      fallback="this will be render on ZodError in children"
    >
      <ThrowZodErrorComponent />
    </ErrorBoundary>
  </ErrorBoundary>
)
```

# !!steps 혹은 반대로 그 Error만 빼고 처리할 수도 있습니다.

그럴 때에는 `shouldCatch`에 callback을 넣어서 처리할 수 있습니다.

```jsx ! Page.jsx
import { ErrorBoundary, ErrorBoundaryGroup } from '@suspensive/react'

const Page = () => (
  <ErrorBoundaryGroup>
    <ErrorBoundaryGroup.Consumer>
      {({ reset }) => <button onClick={reset}>error reset</button>}
    </ErrorBoundaryGroup.Consumer>
    <ErrorBoundary fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
    <DeepComponent />
  </ErrorBoundaryGroup>
)

const DeepComponent = () => (
  <ErrorBoundary fallback="error">
    <ThrowErrorComponent />
    <ErrorBoundary
      shouldCatch={(e) => !(e instanceof ZodError)}
      fallback="this will be render on Error except only ZodError in children"
    >
      <ThrowZodErrorOrErrorComponent />
    </ErrorBoundary>
  </ErrorBoundary>
)
```

</Scrollycoding>

<TrustedBy
  title="사용 중인 기업"
  description="다음 기업에서 Suspensive를 사용하고 있습니다"
/>

<SectionTitle>기여해주신 여러분들께 감사드립니다</SectionTitle>
<br />
<SectionDescription>Driven by the Community</SectionDescription>
<br />

<div
  style={{
    textAlign: 'center',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
  }}
>
  <div style={{ height: 20 }} />
  <Link
    href="https://github.com/toss/suspensive/graphs/contributors"
    target="_blank"
    style={{ textAlign: 'center' }}
  >
    <img src="https://contrib.rocks/image?repo=toss/suspensive" alt="" />
  </Link>
</div>

</HomePage>
